﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ast.Framework.Caching;
using Ast.Framework.Environment.Extensions.Events;
using Ast.Framework.Environment.Extensions.Loaders;
using Ast.Framework.Environment.Extensions.Models;
using Ast.Framework.FileSystems.Dependencies;
using Ast.Framework.FileSystems.Paths;
using Ast.Framework.Utility;
using Castle.Core.Logging;

namespace Ast.Framework.Environment.Extensions
{
    public interface IExtensionLoaderCoordinator
    {
        void SetupExtensions();
    }

    public class ExtensionLoaderCoordinator : IExtensionLoaderCoordinator
    {
        private readonly IDependenciesFolder _dependenciesFolder;
        private readonly IExtensionManager _extensionManager;
        private readonly IParallelCacheContext _parallelCacheContext;
        private readonly IBuildManager _buildManager;
        private readonly IEnumerable<IExtensionLoader> _loaders;
        private readonly IPathProvider _pathProvider;
        private readonly IExtensionEvents _extensionEvents;

        public ExtensionLoaderCoordinator(IDependenciesFolder dependenciesFolder, IExtensionManager extensionManager, IParallelCacheContext parallelCacheContext, IBuildManager buildManager, IEnumerable<IExtensionLoader> loaders, IPathProvider pathProvider, IExtensionEvents extensionEvents)
        {
            _dependenciesFolder = dependenciesFolder;
            _extensionManager = extensionManager;
            _parallelCacheContext = parallelCacheContext;
            _buildManager = buildManager;
            _loaders = loaders;
            _pathProvider = pathProvider;
            _extensionEvents = extensionEvents;

            Logger = NullLogger.Instance;
        }

        public ILogger Logger { get; set; }

        public void SetupExtensions()
        {
            Logger.Info("Start loading extensions...");

            var context = CreateLoadingContext();

            // Notify all loaders about extensions removed from the web site
            foreach (var dependency in context.DeletedDependencies)
            {
                Logger.InfoFormat("Extension {0} has been removed from site", dependency.Name);
                foreach (var loader in _loaders)
                {
                    if (dependency.LoaderName == loader.Name)
                    {
                        loader.ExtensionRemoved(context, dependency);

                        // events
                        _extensionEvents.OnRemoved(context, dependency);
                    }
                }
            }

            // For all existing extensions in the site, ask each loader if they can
            // load that extension.
            foreach (var extension in context.AvailableExtensions)
            {
                ProcessExtension(context, extension);
            }

            // Execute all the work need by "ctx"
            ProcessContextCommands(context);

            // And finally save the new entries in the dependencies folder
            _dependenciesFolder.StoreDescriptors(context.NewDependencies);

            Logger.Info("Done loading extensions...");
        }

        private void ProcessExtensionReference(ExtensionLoadingContext context, ExtensionProbeEntry activatedExtension, string referenceName, IList<DependencyReferenceDescriptor> activatedReferences)
        {
            // If the reference is an extension has been processed already, use the same loader as 
            // that extension, since a given extension should be loaded with a unique loader for the 
            // whole application
            var bestExtensionReference = context.ProcessedExtensions.ContainsKey(referenceName)
                ? context.ProcessedExtensions[referenceName]
                : null;

            // Activated the extension reference
            if (bestExtensionReference != null)
            {
                activatedReferences.Add(new DependencyReferenceDescriptor
                {
                    LoaderName = bestExtensionReference.Loader.Name,
                    Name = referenceName,
                    Path = bestExtensionReference.Path
                });

                return;
            }

            // Skip references from "~/bin"
            if (_buildManager.HasReferencedAssembly(referenceName)) return;

            // Binary references
            var references = context.ReferencesByName.ContainsKey(referenceName)
                ? context.ReferencesByName[referenceName]
                : Enumerable.Empty<ExtensionReferenceProbeEntry>();

            var bestBinaryReference = references
                .Where(entry => !string.IsNullOrEmpty(entry.Path))
                .Select(entry => new {Entry = entry, LastWriteTimeUtc = _pathProvider.GetFileLastWriteTimeUtc(entry.Path)})
                .OrderBy(e => e.LastWriteTimeUtc)
                .ThenBy(e => e.Entry.Name)
                .FirstOrDefault();

            // Activate the binary ref
            if (bestBinaryReference != null)
            {
                if (!context.ProcessedReferences.ContainsKey(bestBinaryReference.Entry.Name))
                {
                    context.ProcessedReferences.Add(bestBinaryReference.Entry.Name, bestBinaryReference.Entry);
                    bestBinaryReference.Entry.Loader.ReferenceActivated(context, bestBinaryReference.Entry);
                }
                activatedReferences.Add(new DependencyReferenceDescriptor
                {
                    LoaderName = bestBinaryReference.Entry.Loader.Name,
                    Name = bestBinaryReference.Entry.Name,
                    Path = bestBinaryReference.Entry.Path
                });
            }
        }

        private IEnumerable<DependencyReferenceDescriptor> ProcessExtensionReferences(ExtensionLoadingContext context, ExtensionProbeEntry activatedExtension)
        {
            if (activatedExtension == null) return Enumerable.Empty<DependencyReferenceDescriptor>();

            var referenceNames = (context.ReferencesByModule.ContainsKey(activatedExtension.Descriptor.Id)
                ? context.ReferencesByModule[activatedExtension.Descriptor.Id]
                : Enumerable.Empty<ExtensionReferenceProbeEntry>())
                .Select(r => r.Name)
                .Distinct(StringComparer.OrdinalIgnoreCase);

            var referencesDecriptors = new List<DependencyReferenceDescriptor>();
            foreach (var referenceName in referenceNames)
            {
                ProcessExtensionReference(context, activatedExtension, referenceName, referencesDecriptors);
            }

            return referencesDecriptors;
        }

        private void ProcessExtension(ExtensionLoadingContext context, ExtensionDescriptor extension)
        {
            var extensionProbes = context.AvailableExtensionsProbes.ContainsKey(extension.Id)
                ? context.AvailableExtensionsProbes[extension.Id]
                : Enumerable.Empty<ExtensionProbeEntry>();

            // materializes the list
            extensionProbes = extensionProbes.ToArray();

            if (Logger.IsDebugEnabled)
            {
                Logger.DebugFormat("Loaders for extension \"{0}\": ", extension.Id);
                foreach (var probe in extensionProbes)
                {
                    Logger.DebugFormat("  Loader: {0}", probe.Loader.Name);
                    Logger.DebugFormat("    Path: {0}", probe.Path);
                    Logger.DebugFormat("    PathDependencies: {0}", string.Join(", ", probe.PathDependencies));
                }
            }

            var moduleReferences = context.AvailableExtensions
                .Where(e =>
                    context.ReferencesByModule.ContainsKey(extension.Id) &&
                    context.ReferencesByModule[extension.Id].Any(r => StringComparer.OrdinalIgnoreCase.Equals(e.Id, r.Name)))
                .ToList();

            var processedModuleReferences = moduleReferences
                .Where(e => context.ProcessedExtensions.ContainsKey(e.Id))
                .Select(e => context.ProcessedExtensions[e.Id])
                .ToList();

            var activatedExtension = extensionProbes.FirstOrDefault(e => e.Loader.IsCompatibleWithModuleReferences(extension, processedModuleReferences));

            var previousDependency = context.PreviousDependencies.FirstOrDefault(d => StringComparer.OrdinalIgnoreCase.Equals(d.Name, extension.Id));

            if (activatedExtension == null)
            {
                Logger.WarnFormat("No loader found for extension \"{0}\"!", extension.Id);
            }

            var references = ProcessExtensionReferences(context, activatedExtension);

            foreach (var loader in _loaders)
            {
                if (activatedExtension != null && activatedExtension.Loader.Name == loader.Name)
                {
                    Logger.InfoFormat("Activating extension \"{0}\" with loader \"{1}\"", activatedExtension.Descriptor.Id, loader.Name);
                    loader.ExtensionActivated(context, extension);
                }
                else if (previousDependency != null && previousDependency.LoaderName == loader.Name)
                {
                    Logger.InfoFormat("Deactivating extension \"{0}\" from loader \"{1}\"", previousDependency.Name, loader.Name);
                    loader.ExtensionDeactivated(context, extension);

                    // events
                    _extensionEvents.OnDeactivated(context, previousDependency);
                }
            }

            if (activatedExtension != null)
            {
                var dependencyDescriptor = new DependencyDescriptor
                {
                    Name = extension.Id,
                    LoaderName = activatedExtension.Loader.Name,
                    Path = activatedExtension.Path,
                    References = references
                };
                context.NewDependencies.Add(dependencyDescriptor);

                // events
                _extensionEvents.OnActivated(context, dependencyDescriptor);
            }

            // Keep track of which loader we use for every extension
            // This will be needed for processing references from other dependent extensions
            context.ProcessedExtensions.Add(extension.Id, activatedExtension);
        }

        private ExtensionLoadingContext CreateLoadingContext()
        {
            var availableExtensions = _extensionManager
                .AvailableExtensions()
                .Where(d => DefaultExtensionTypes.IsModule(d.ExtensionType))
                .OrderBy(d => d.Id)
                .ToList();

            // Check there are no duplicates
            var duplicates = availableExtensions.GroupBy(ed => ed.Id).Where(g => g.Count() >= 2).ToList();
            if (duplicates.Any())
            {
                var sb = new StringBuilder();
                sb.Append("There are multiple extensions with the same name installed in this instance.\r\n");
                foreach (var dup in duplicates)
                {
                    sb.AppendFormat("Extension '{0}' has been found from the following locations: {1}.\r\n", dup.Key, string.Join(", ", dup.Select(e => e.Location + "/" + e.Id)));
                }
                sb.Append("This issue can be usually solved by removing or renaming the conflicting extension.");
                var error = sb.ToString();
                Logger.Error(error);
                throw new Exception(error);
            }

            var previousDependencies = _dependenciesFolder.LoadDescriptors().ToList();

            var pathModficationDates = new ConcurrentDictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);

            Logger.Info("Probing extensions");
            var availableExtensionsProbes1 = _parallelCacheContext
                .RunInParallel(availableExtensions, extension => _loaders.Select(loader => loader.Probe(extension)).Where(entry => entry != null).ToArray())
                .SelectMany(entries => entries)
                .GroupBy(entry => entry.Descriptor.Id);

            var availableExtensionsProbes = _parallelCacheContext
                .RunInParallel(availableExtensionsProbes1, g => new {Id = g.Key, Entries = SortExtensionProbeEntries(g, pathModficationDates)})
                .ToDictionary(g => g.Id, g => g.Entries, StringComparer.OrdinalIgnoreCase);
            Logger.Info("Done probing extensions");

            var deletedDependencies = previousDependencies
                .Where(e => !availableExtensions.Any(e2 => StringComparer.OrdinalIgnoreCase.Equals(e2.Id, e.Name)))
                .ToList();

            // Collect references for all modules
            Logger.Info("Probing extension references");
            var references = _parallelCacheContext
                .RunInParallel(availableExtensions, extension => _loaders.SelectMany(loader => loader.ProbeReferences(extension)).ToList())
                .SelectMany(entries => entries)
                .ToList();
            Logger.Info("Done probing extension references");

            var referencesByModule = references
                .GroupBy(entry => entry.Descriptor.Id, StringComparer.OrdinalIgnoreCase)
                .ToDictionary(g => g.Key, g => g.AsEnumerable(), StringComparer.OrdinalIgnoreCase);

            var referencesByName = references
                .GroupBy(reference => reference.Name, StringComparer.OrdinalIgnoreCase)
                .ToDictionary(g => g.Key, g => g.AsEnumerable(), StringComparer.OrdinalIgnoreCase);

            var sortedAvailableExtensions = availableExtensions
                .OrderByDependenciesAndPriorities((item, dep) => referencesByModule.ContainsKey(item.Id) && referencesByModule[item.Id].Any(r => StringComparer.OrdinalIgnoreCase.Equals(dep.Id, r.Name)), item => 0)
                .ToList();

            return new ExtensionLoadingContext
            {
                AvailableExtensions = sortedAvailableExtensions,
                PreviousDependencies = previousDependencies,
                DeletedDependencies = deletedDependencies,
                AvailableExtensionsProbes = availableExtensionsProbes,
                ReferencesByName = referencesByName,
                ReferencesByModule = referencesByModule,
                PathModficationDates = pathModficationDates,
            };
        }

        private IEnumerable<ExtensionProbeEntry> SortExtensionProbeEntries(IEnumerable<ExtensionProbeEntry> entries, ConcurrentDictionary<string, DateTime> pathModficationDates)
        {
            // All "entries" are for the same extension ID, so we just need to filter/sort them by priority+ modification dates.
            var groupByPriority = entries
                .GroupBy(entry => entry.Priority)
                .OrderByDescending(g => g.Key);

            // Select highest priority group with at least one item
            var firstNonEmptyGroup = (groupByPriority.FirstOrDefault(g => g.Any()) ?? Enumerable.Empty<ExtensionProbeEntry>()).ToList();

            // No need for further sorting if only 1 item found
            if (firstNonEmptyGroup.Count <= 1) return firstNonEmptyGroup;

            // Sort by last modification date/loader order
            return firstNonEmptyGroup
                .OrderByDescending(probe => GetPathDepedenciesModificationTimeUtc(pathModficationDates, probe))
                .ThenBy(probe => probe.Loader.Order)
                .ToList();
        }

        private DateTime GetPathDepedenciesModificationTimeUtc(ConcurrentDictionary<string, DateTime> pathDependencies, ExtensionProbeEntry probe)
        {
            if (!probe.PathDependencies.Any()) return DateTime.MinValue;

            Logger.InfoFormat("Retrieving modification dates of dependencies of extension '{0}'", probe.Descriptor.Id);
            var result = probe.PathDependencies.Max(path => GetPathModificationTimeUtc(pathDependencies, path));
            Logger.InfoFormat("Done retrieving modification dates of dependencies of extension '{0}'", probe.Descriptor.Id);
            return result;
        }

        private DateTime GetPathModificationTimeUtc(ConcurrentDictionary<string, DateTime> pathDependencies, string path)
        {
            return pathDependencies.GetOrAdd(path, p => _pathProvider.GetFileLastWriteTimeUtc(p));
        }

        private void ProcessContextCommands(ExtensionLoadingContext ctx)
        {
            Logger.Info("Executing list of operations needed for loading extensions...");
            foreach (var action in ctx.DeleteActions)
            {
                action();
            }

            foreach (var action in ctx.CopyActions)
            {
                action();
            }
        }
    }
}